use std::ffi::{c_void, CString};
use std::os::raw::c_char;
use std::ptr;
use std::time::Instant;
use x11::xlib::{
    AnyPropertyType, Atom, Display, XCloseDisplay, XDefaultRootWindow, XFetchName, XFree,
    XGetWindowProperty, XInternAtom, XOpenDisplay, XID,
};

pub struct WindowList {
    display_handle: *mut Display,
    last_update: Instant,
    pub results: Vec<WindowInfo>,
}

impl WindowList {
    pub fn new() -> WindowList {
        let mut wl = WindowList {
            display_handle: open_display(None).expect("unable to open Display"),
            last_update: Instant::now(),
            results: vec![],
        };
        wl.update();
        wl
    }
    pub fn update(&mut self) -> bool {
        #[allow(non_snake_case)]
        let Success = 0;
        #[allow(non_snake_case)]
        let True = 1;
        #[allow(non_snake_case)]
        let False = 0;

        let mut results = vec![];

        let ncl = unsafe {
            XInternAtom(
                self.display_handle,
                b"_NET_CLIENT_LIST\0".as_ptr() as *const c_char,
                True,
            )
        };

        let root_window = unsafe { XDefaultRootWindow(self.display_handle) };

        let mut actual_type: Atom = Default::default();
        let mut format: i32 = 0;
        let mut num_items: u64 = 0;
        let mut bytes_after: u64 = 0;
        let mut data = ptr::null_mut();
        let status = unsafe {
            XGetWindowProperty(
                self.display_handle,
                root_window,
                ncl,
                0,
                !0,
                False,
                AnyPropertyType as u64,
                &mut actual_type,
                &mut format,
                &mut num_items,
                &mut bytes_after,
                &mut data,
            )
        };
        //println!("WindowList::update::status = {}, num_items = {}", status, num_items);
        if status >= Success && num_items >= 1 {
            assert_eq!(32, format, "format should always be 32");
            let ptr = data as *const XID;
            let slice = unsafe { std::slice::from_raw_parts(ptr, num_items as usize) };

            for id in slice {
                let mut name = ptr::null_mut();
                let status = unsafe { XFetchName(self.display_handle, *id, &mut name) };
                if status >= Success {
                    if name == ptr::null_mut() {
                        //println!("lookup failed");
                    } else {
                        let title = unsafe {
                            let mut len: isize = 0;
                            while *name.offset(len) != 0 {
                                len += 1;
                            }

                            let slice = std::slice::from_raw_parts(name as *const u8, len as usize);
                            textcode::iso8859_1::decode_to_string(slice)
                        };

                        let trimmed = title.trim();
                        if trimmed != "" {
                            let title = trimmed.into();
                            results.push(WindowInfo { id: *id, title });
                        }

                        unsafe {
                            XFree(name as *mut c_void);
                        }
                    }
                }
            }
        }

        unsafe {
            XFree(data as *mut c_void);
        }

        if self.results != results {
            self.results = results;
            self.last_update = Instant::now();
            true
        } else {
            false
        }
    }
}

impl Drop for WindowList {
    fn drop(&mut self) {
        unsafe {
            XCloseDisplay(self.display_handle);
        }
    }
}

#[derive(PartialEq)]
pub struct WindowInfo {
    pub id: XID,
    pub title: String,
}

pub fn open_display(name: Option<CString>) -> Option<*mut Display> {
    unsafe {
        let name = match name {
            None => ptr::null(),
            Some(cstr) => cstr.as_ptr(),
        };
        let d = XOpenDisplay(name);

        if d.is_null() {
            return None;
        }

        Some(d)
    }
}
